Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix and extend wcs1d-fits loader for multi-D WCS #1009

Merged
merged 5 commits into from
Mar 17, 2023

Conversation

dhomeier
Copy link
Contributor

@dhomeier dhomeier commented Feb 14, 2023

To address #592 (comment); quick fix for enabling writing of spectral cubes with celestial components in WCS.
Reading in with the full WCS is now supported as well, plus adding mask and uncertainty to the attributes read from file.
The latter at this point are still limited to the format storing them all in different ImageHDUs; an alternative common format storing the fields as sub-arrays of an N+1 image yet to be implemented.
Fixes #592

@codecov
Copy link

codecov bot commented Feb 14, 2023

Codecov Report

Attention: Patch coverage is 84.88372% with 13 lines in your changes missing coverage. Please review.

Project coverage is 70.26%. Comparing base (8179313) to head (1d5a59f).
Report is 89 commits behind head on main.

Files with missing lines Patch % Lines
specutils/io/default_loaders/wcs_fits.py 84.88% 13 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1009      +/-   ##
==========================================
+ Coverage   70.01%   70.26%   +0.24%     
==========================================
  Files          64       64              
  Lines        4346     4419      +73     
==========================================
+ Hits         3043     3105      +62     
- Misses       1303     1314      +11     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@pllim
Copy link
Member

pllim commented Feb 14, 2023

So this fixes #592 ?

@dhomeier
Copy link
Contributor Author

This should work for your example.
Reading the full WCS in rather than only the spectral dimension may of course have consequences if spectrum.wcs is used at a later point. Another immediate change is that this will now only accept the official FITS spectral axis keywords recognised my WCSLIB, FREQ, AFRQ, ENER, WAVN,VRAD, WAVE, VOPT, ZOPT, AWAV, VELO, BETA.
The tests are adapted, but if downstream applications have used names like wavelength or wavenumber, this may need additional pre-processing of the header.

@dhomeier
Copy link
Contributor Author

I was somewhat surprised when updating the writer, that there are apparently WCS that have a spectral component, yet
wcs.spectral.all_pix2world(pix) returns a different (wrong) result than wcs.all_pix2world(pix) (the former fails on all other tests that only input a 1D, pure spectral WCS). But the check should probably be there anyway, if it ever has to deal with non-FITS WCS.

@@ -707,11 +707,10 @@ def test_tabular_fits_compressed(compress, tmpdir):


@pytest.mark.parametrize("spectral_axis",
['wavelength', 'frequency', 'energy', 'wavenumber'])
['WAVE', 'FREQ', 'ENER', 'WAVN'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some minor context: if I recall, the spectral axis names were meant to correspond with the class properties and not the FITS naming standard. Either way, it's not an issue, but figured the history of the usage might add some context.

@dhomeier
Copy link
Contributor Author

I have added read and write for spectrum.mask and given the HDUs names, handling it I think more similar to how the cubeviz loader treats its files. Things like detection strategy for the spectrum HDU (and mask? Should it be found under other names as well?) may need some more discussion, also perhaps more test coverage for the naming schemes.
In principle this could probably extended in a similar way to also handle uncertainties.

@dhomeier
Copy link
Contributor Author

Apparently something in oldestdeps is pulling numpy 1.24.2 in, overriding numpy==1.19 and breaking the old scipy version. I guess that means 1.19 is not really supported anymore, but don't know what it is that's requiring a newer version.

@pllim
Copy link
Member

pllim commented Feb 15, 2023

Apparently something in oldestdeps is pulling numpy 1.24.2

Hmm easy way out is to bump min version of numpy though even that might not work. Another thing you can do is to hunt down what is overwriting the numpy pin and pin that package to an older version as well.

extended in a similar way to also handle uncertainties

Definitely needed if you want it to be generic enough since Spectrum1D does have an uncertainty component. I just didn't do it downstream because apparently model fitting in Cubeviz does not generate that.

@dhomeier dhomeier force-pushed the wcs1d-3d branch 3 times, most recently from de33ac4 to 20f9de7 Compare February 18, 2023 23:08
@dhomeier dhomeier changed the title Fix wcs1d-fits loader for multi-D WCS Fix and extendwcs1d-fits loader for multi-D WCS Mar 3, 2023
@dhomeier dhomeier marked this pull request as ready for review March 3, 2023 15:19
@dhomeier
Copy link
Contributor Author

dhomeier commented Mar 3, 2023

Marking this as ready for review now, as the multi-HDU format for masks and uncertainties is functional (unless there are more labels the loader needs to identify).
The format reading different attributes from data slices in a single HDU, which would work similarly to 6dfgs

flux = hdu.data[0] * Unit("count") / w.wcs.cunit[0]
uncertainty = VarianceUncertainty(hdu.data[1])

could still be added here or in a follow-up PR.

@pllim pllim changed the title Fix and extendwcs1d-fits loader for multi-D WCS Fix and extend wcs1d-fits loader for multi-D WCS Mar 3, 2023
verbose : bool
hdu : int, str or None, optional
The index or name of the HDU to load into this spectrum
(default: find 1st applicable `ImageHDU`).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me. Is this loader only for image? A spectrum can also come as table HDU, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wcs1d handles only spectra with flux represented as image and spectral axis defined by the WCS; tables are covered by tabular-fits. That already supports masks and uncertainties, more or less, which are simply additional table columns in that representation.

uncertainty_name : str or `None`, optional
HDU name to store uncertainty under (default set from type; `None`: do not save)
unit : str or :class:`~astropy.units.Unit`, optional
Unit for the flux (and associated uncertainty; defaults to `spectrum.flux.unit`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless spectrum.flux.unit actually generates API doc, use double-backticks. I cannot check because RTD build failed on your PR. Also see #1028.

Copy link
Contributor Author

@dhomeier dhomeier Mar 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spectrum.flux does, but not specifically unit, is that what you mean? I don't know if the loaders themselves generate API docs, or rather where they might be linked from ;-)
Thanks for pointing out the RTD failure; throws in all current PRs and does not seem related – from a quick glance some parsing error related to reproject, but needs further investigation – oh, it already does!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it seems none of the functions in default_loaders have any docs built at all, though it would certainly be useful to have them and link them in https://specutils.readthedocs.io/en/latest/spectrum1d.html#list-of-loaders.

@pllim
Copy link
Member

pllim commented Mar 15, 2023

Sorry, I think my fixes to the tests caused a conflict here, so you need to rebase and resolve the conflict.

@dhomeier dhomeier force-pushed the wcs1d-3d branch 2 times, most recently from d5705fa to 696a0b3 Compare March 15, 2023 18:06
@dhomeier
Copy link
Contributor Author

Doh, that seems to have screwed up more commits than I even had in this branch!

@@ -875,7 +1026,7 @@ def test_wcs1d_fits_compressed(compress, tmp_path):
# Try again without compression suffix:
with warnings.catch_warnings():
warnings.simplefilter('ignore', FITSFixedWarning)
os.system(f'mv {tmpfile}{ext[compress]} {tmpfile}')
os.system(f'mv {tmpfile}.{ext[compress]} {tmpfile}')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note: Why call os.system instead of shutil? Anyways, I guess fixing this is out of scope.

Copy link
Contributor Author

@dhomeier dhomeier Mar 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps because I (or whoever wrote the tests originally) did not know any better at the time, or perhaps because os.system is needed anyway to do the compression – or is there also some kind of exec functionality with shutil?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how "mv" cares about compression? https://docs.python.org/3/library/shutil.html#shutil.move

Copy link
Contributor Author

@dhomeier dhomeier Mar 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not at all, I guess 😄; just meant that it's kind of the direct counterpart to
os.system(f'{compress} {tmpfile}') just above, and that cannot be replaced with shutil.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway there are a couple of those compress-rename duos, so refactoring them would be better left to a separate PR.

Copy link
Member

@pllim pllim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left what general review comments I could. Now it is up to specutils maintainers to review. Thanks!

@@ -157,12 +246,16 @@ def wcs1d_fits_writer(spectrum, file_name, hdu=0, update_header=False, **kwargs)
raise ValueError(f'Only Spectrum1D objects with valid WCS can be written as wcs1d: {err}')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not that familiar with the workings of WCS (and especially wcs.to_fits()) - is there any realistic scenario for setting hdu to a value other than 0, given that the initial hdulist is always created from the spectrum's WCS anyway? I guess what I'm asking is, is there really utility in having empty HDUs in front of the specified hdu index? There are no options to specify the HDU numbers for uncertainty and mask, so it's not like the user could write out in order [uncertainty, mask, flux], for example. Maybe we just get rid of the hdu argument altogether?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as this is not considered "public" API and ok to break just like that, I agree.

Copy link
Contributor Author

@dhomeier dhomeier Mar 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not that familiar with the workings of WCS (and especially wcs.to_fits()) - is there any realistic scenario for setting hdu to a value other than 0, given that the initial hdulist is always created from the spectrum's WCS anyway?

It's not as much associated to using a WCS, but rather to the flux being stored in an ImageHDU, which can be the PrimaryHDU or an extension, while tables can only be stored in extensions. However for symmetry reasons or whatever sometimes writing the image to an extension, too, and using just a basic PrimaryHDU, is desired (there was one comment somewhere sketching just this scenario, but I cannot find it either in the Slack discussion or any related issue now).
For the record: that example is right in #592 (comment) that set this whole thing off.
I think originally accepting any value for hdu was just introduced as a poor substitute for not being able to write multiple spectra to one file/hdulist, but there is probably no real use case for this. For hdu=1 I'd say there is, and this should still remain available.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation...I guess it makes sense to leave it in then for now.

@rosteen
Copy link
Contributor

rosteen commented Mar 16, 2023

Thanks @dhomeier, this is a great improvement. I had one question and left one suggestion, and there's one suggestion from @pllim remaining, but other than that I think this looks good. I appreciate the test coverage.

I was going to say, maybe we change the name from wcs1d_fits to just wcs_fits if it handles multi-D WCS now, but perhaps we go with the standard answer of "1D means one spectral dimension" like in Spectrum1D.

@dhomeier
Copy link
Contributor Author

I was going to say, maybe we change the name from wcs1d_fits to just wcs_fits if it handles multi-D WCS now, but perhaps we go with the standard answer of "1D means one spectral dimension" like in Spectrum1D.

I had been thinking about that some times; to me the distinctive property was actually usually having the flux as Image data rather than a table column, so having something like image-fits as counterpart to tabular-fits seemed more sensible.
But I also think the "1D spectral" designation is sensible enough, and changing it is probably not worth deprecating the old label.

@rosteen
Copy link
Contributor

rosteen commented Mar 17, 2023

@dhomeier Can I commit the last suggestions for you and merge this?

Copy link
Contributor

@rosteen rosteen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved, need to commit last two suggestions before merging.

Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com>
@rosteen rosteen merged commit f5db745 into astropy:main Mar 17, 2023
@pllim
Copy link
Member

pllim commented Mar 17, 2023

Hmm does this mean we should ditch Jdaviz custom writer and use this?

@dhomeier
Copy link
Contributor Author

@dhomeier Can I commit the last suggestions for you and merge this?

I've included them in some more local updates I had made; should be good to merge then.

@rosteen
Copy link
Contributor

rosteen commented Mar 17, 2023

@dhomeier Can I commit the last suggestions for you and merge this?

I've included them in some more local updates I had made; should be good to merge then.

Blast, I went ahead and committed and merged, thinking you might be done and logged off for the day. Are your other updates worth opening a follow-up PR for? Sorry about that.

@dhomeier
Copy link
Contributor Author

Ah, remote tests are taking too long here (spPlate really bogs everything down).
Will think about the follow-up, the extra test might be good to have, but now I am going to be offline for a while.

@pllim
Copy link
Member

pllim commented Mar 17, 2023

Thanks, @dhomeier ! I am trying out this improved format over at spacetelescope/jdaviz#2094 but I encountered error. Perhaps we need an extra step to convert bool mask to uint16 (I guess it could be uint8 but uint16 is common in bit-plane format)?

>       fitted_spectrum.write(out_fn, format="wcs1d-fits", overwrite=True)

jdaviz/configs/default/plugins/model_fitting/tests/test_fitting.py:229: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
astropy/nddata/mixins/ndio.py:103: in __call__
    self.registry.write(self._instance, *args, **kwargs)
astropy/io/registry/core.py:383: in write
    return writer(data, *args, **kwargs)
specutils/io/default_loaders/wcs_fits.py:278: in wcs1d_fits_writer
    hdulist[-1].data = spectrum.mask
astropy/utils/decorators.py:848: in __set__
    ret = self.fset(obj, val)
...
E           KeyError: 'bool'

https://github.com/spacetelescope/jdaviz/actions/runs/4451633462/jobs/7818514722?pr=2094

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Writer needed for cube data
4 participants